package org.anderground.app.storage.global;

import java.io.IOException;
import java.util.LinkedList;
import java.util.List;

import org.anderground.app.storage.AppStorage;
import org.anderground.app.storage.ConfigData;
import org.anderground.app.storage.NotYetConfiguredException;
import org.anderground.app.storage.OnProgressCallback;
import org.anderground.app.storage.ConfigData.ConfigCity;
import org.anderground.core.Logger;
import org.anderground.core.model.ModelData;
import org.anderground.core.storage.StorageException;

/**
 * @author Eldar Abusalimov
 */
public class GlobalStorageProxy extends AppStorage {

	/**
	 * @author Eldar Abusalimov
	 */
	public interface OnDoubleProgressCallback extends OnProgressCallback {

		public abstract void onSecondaryProgress(int secondaryProgress,
				int total, String description);

	}

	private final AppStorage[] storages;

	private static GlobalStorageProxy instance = null;

	/**
	 * @return the instance
	 * @throws NotYetConfiguredException
	 */
	public static GlobalStorageProxy getInstance()
			throws NotYetConfiguredException {
		if (instance == null || instance.getConfigData() == null) {
			throw new NotYetConfiguredException();
		}
		return instance;
	}

	/**
	 * @param storages
	 * @param onProgressCallback
	 * @return the instance
	 * @throws IOException
	 */
	static GlobalStorageProxy createAndConfigureInstance(AppStorage[] storages,
			OnDoubleProgressCallback onProgressCallback) throws IOException {
		instance = new GlobalStorageProxy(storages);
		if (readConfigFromStorage(instance, onProgressCallback) == null) {
			throw new StorageException("null ConfigData was returned");
		}
		return instance;
	}

	static boolean tryConfiguratingDummyInstance(AppStorage[] storages) {
		try {
			return readConfigFromStorage(new GlobalStorageProxy(storages), null) != null;
		} catch (IOException e) {
			return false;
		}
	}

	/**
	 * @param storages
	 */
	private GlobalStorageProxy(AppStorage[] storages) {
		if (storages == null) {
			throw new IllegalArgumentException("storages array cannot be null");
		}
		this.storages = new AppStorage[storages.length];
		System.arraycopy(storages, 0, this.storages, 0, storages.length);
	}

	@Override
	protected ModelData readModelImpl(ConfigCity city) throws IOException {
		return readModelFromStorages(storages, city);
	}

	@Override
	protected void writeModelImpl(ModelData data, ConfigCity city)
			throws IOException {
		writeModelToStorages(storages, data, city);
	}

	/**
	 * 
	 * @param storages
	 * @param city
	 * @return
	 * @throws IOException
	 */
	private static ModelData readModelFromStorages(AppStorage[] storages,
			ConfigCity city) throws IOException {
		ModelData data = null;
		List<AppStorage> missingDataStorages = new LinkedList<AppStorage>();
		for (AppStorage storage : storages) {
			try {
				Logger.d("model reading << "
						+ storage.getClass().getSimpleName());
				missingDataStorages.add(0, storage);
				if ((data = readFromStorage(storage, city)) != null) {
					Logger.v("..success");
					missingDataStorages.remove(0);
					break;
				}
			} catch (IOException e) {
				Logger.v("..failed: " + e.getMessage());
			} catch (Throwable e) {
				Logger.w("model reading failed with unexpected exception: ", e);
			}
		}
		if (data == null) {
			throw new StorageException(
					"None of readers could not read the model");
		}
		try {
			writeModelToStorages(missingDataStorages
					.toArray(new AppStorage[missingDataStorages.size()]), data,
					city);
		} catch (IOException e) {
			// noop
		}
		return data;
	}

	/**
	 * @param storages
	 * @param data
	 * @param city
	 * @throws IOException
	 */
	private static void writeModelToStorages(AppStorage[] storages,
			ModelData data, ConfigCity city) throws IOException {
		boolean written = false;
		for (AppStorage storage : storages) {
			try {
				Logger.d("model writing >> "
						+ storage.getClass().getSimpleName());
				writeToStorage(storage, data, city);
				Logger.v("..success");
				written = true;
			} catch (IOException e) {
				Logger.v("..failed: " + e.getMessage());
			} catch (Throwable e) {
				Logger.w("model writing failed with unexpected exception: ", e);
			}
		}
		if (!written) {
			throw new StorageException(
					"None of writers could not write the model");
		}
	}

	@Override
	protected ConfigData readConfigImpl(
			final OnProgressCallback onProgressCallback) throws IOException {
		return readConfigFromStorages(storages, onProgressCallback);
	}

	@Override
	protected void writeConfigImpl(ConfigData data,
			OnProgressCallback onProgressCallback) throws IOException {
		writeConfigToStorages(storages, data, onProgressCallback);
	}

	/**
	 * 
	 * @param storages
	 * @param c
	 * @return
	 * @throws IOException
	 */
	private static ConfigData readConfigFromStorages(AppStorage[] storages,
			final OnProgressCallback c) throws IOException {
		ConfigData data = null;
		List<AppStorage> missingDataStorages = new LinkedList<AppStorage>();
		final int total = storages.length;
		int current = 0;
		for (AppStorage storage : storages) {
			try {
				missingDataStorages.add(0, storage);
				String storageName = storage.getClass().getSimpleName();
				String description = "Reading from " + storageName;
				fireOnProgressCallback(c, 0, total, null);
				fireOnSecondaryProgressCallback(c, current, total, description);
				OnProgressCallback callbackProxy = createOnProgressCallbackProxy(
						c, description);
				Logger.d("config reading << " + storageName);
				if ((data = readConfigFromStorage(storage, callbackProxy)) != null) {
					Logger.v("..success");
					missingDataStorages.remove(0);
					break;
				}
			} catch (IOException e) {
				Logger.v("..failed: " + e.getMessage());
			} catch (Throwable e) {
				Logger
						.w("config reading failed with unexpected exception: ",
								e);
			}
			current++;
		}
		if (data == null) {
			fireOnProgressCallback(c, 0, total, null);
			fireOnSecondaryProgressCallback(c, 0, total, "Reading failed");
			throw new IOException("None of storages have been configured");
		}
		fireOnProgressCallback(c, total, total, null);
		fireOnSecondaryProgressCallback(c, total, total, "Reading completed");
		try {
			if (!missingDataStorages.isEmpty()) {
				AppStorage[] storagesToWriteIn = missingDataStorages
						.toArray(new AppStorage[missingDataStorages.size()]);
				writeConfigToStorages(storagesToWriteIn, data, c);
			}
		} catch (IOException e) {
			// noop
		}
		return data;
	}

	/**
	 * @param storages
	 * @param data
	 * @param c
	 * @throws IOException
	 */
	private static void writeConfigToStorages(AppStorage[] storages,
			ConfigData data, final OnProgressCallback c) throws IOException {
		boolean written = false;
		final int total = storages.length;
		int current = 0;
		for (AppStorage storage : storages) {
			try {
				String storageName = storage.getClass().getSimpleName();
				String description = "Writing to " + storageName;
				fireOnProgressCallback(c, 0, total, null);
				fireOnSecondaryProgressCallback(c, current, total, description);
				OnProgressCallback callbackProxy = createOnProgressCallbackProxy(
						c, description);
				Logger.d("config writing >> " + storageName);
				writeConfigToStorage(storage, data, callbackProxy);
				Logger.v("..success");
				written = true;
			} catch (IOException e) {
				Logger.v("..failed: " + e.getMessage());
			} catch (Throwable e) {
				Logger
						.w("config writing failed with unexpected exception: ",
								e);
			}
			current++;
		}
		if (!written) {
			fireOnProgressCallback(c, 0, total, null);
			fireOnSecondaryProgressCallback(c, 0, total, "Writing failed");
			throw new StorageException("None of storages were configured");
		}
		fireOnProgressCallback(c, total, total, null);
		fireOnSecondaryProgressCallback(c, total, total, "Writing completed");
	}

	private static OnProgressCallback createOnProgressCallbackProxy(
			final OnProgressCallback onProgressCallback,
			final String descrPrefix) {
		return new OnProgressCallback() {
			@Override
			public void onProgress(int progress, int max, String description) {
				String message = descrPrefix;
				if (description != null) {
					message += "\n" + description;
				}
				fireOnProgressCallback(onProgressCallback, progress, max,
						message);
			}

			@Override
			public void onProgress(String description) {
				String message = descrPrefix;
				if (description != null) {
					message += "\n" + description;
				}
				if (onProgressCallback == null) {
					return;
				}
				onProgressCallback.onProgress(message);
			}

		};
	}

	/**
	 * 
	 * @param onProgressCallback
	 * @param current
	 * @param total
	 * @param description
	 */
	private static void fireOnProgressCallback(
			final OnProgressCallback onProgressCallback, int current,
			final int total, String description) {
		if (onProgressCallback == null) {
			return;
		}
		onProgressCallback.onProgress(current, total, description);
	}

	/**
	 * 
	 * @param onProgressCallback
	 * @param current
	 * @param total
	 * @param description
	 */
	private static void fireOnSecondaryProgressCallback(
			final OnProgressCallback onProgressCallback, int current,
			final int total, String description) {
		if (onProgressCallback == null) {
			return;
		}
		if (onProgressCallback instanceof OnDoubleProgressCallback) {
			((OnDoubleProgressCallback) onProgressCallback)
					.onSecondaryProgress(current, total, description);
		}
	}

}
